package org.upfrost.mapping;

import java.beans.IntrospectionException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;

import org.upfrost.Mapper;
import org.upfrost.annotation.Column;
import org.upfrost.annotation.Table;
import org.upfrost.util.Pair;



public class ClassAnalyzer 
{
	static public ClassProxy buildProxy(Class c, Mapper mapper) throws IntrospectionException
	{
		ClassProxy result = new ClassProxy(c, mapper);
		buildProxy(result, c, mapper);
		return result;
	}
	static public ClassProxy buildProxy(ClassProxy result, Class c, Mapper mapper) throws IntrospectionException
	{
		// Inheritance management
		Class superclass = c.getSuperclass();
		if (superclass.equals(Object.class) == false)
		{
			buildProxy(result, superclass, mapper);
		}

		if (c.isAnnotationPresent(Table.class))
		{
			Table table = (Table) c.getAnnotation(Table.class);
			result.setTableName(table.value());
		} else if (c.isAnnotationPresent(javax.persistence.Table.class))
		{
			javax.persistence.Table table = (javax.persistence.Table)c.getAnnotation(javax.persistence.Table.class);
			result.setTableName(table.name());
		}

		boolean annotationsInFields = false;
		boolean annotationsInMethods = false;

		// We check private and public fields
		for (Field f: c.getDeclaredFields())
		{
			if (isKnownAnnotation(f.getAnnotations()))
			{
				annotationsInFields = true;
				break;
			}
		}

		// We only check public methods
		for (Method m: c.getMethods())
		{
			if (isKnownAnnotation(m.getAnnotations()))
			{
				annotationsInMethods = true;
				break;
			}
		}

		if (annotationsInFields==false && annotationsInMethods==false) // default is methods
		{
			annotationsInMethods = true;
		}

		if (annotationsInFields==true && annotationsInMethods==true)
			throw new RuntimeException("Can't mix mapping annotation in both fields and accessor methods");

		LinkedList<Mapping> resultMapping = new LinkedList<Mapping>();
		
		if (annotationsInFields)
		{
			for (Field f: c.getDeclaredFields())
			{
				if (f.getDeclaringClass().equals(Object.class)==false &&  ! Modifier.isStatic(f.getModifiers()))
				{
					Mapping candidate = new Mapping(c, f, mapper);
					setupMappingAnnotations(result, resultMapping, candidate, f.getAnnotations());			
				}
				// manageField(f, c, result);
			}
		}

		if (annotationsInMethods)
		{
			for (Method m: c.getDeclaredMethods())
			{
				Pair<String,Class<?>> prop = getPropertyFromMethodName(m);

				// If we get a valid getter/setter, we also check the type consistency between both the setter and the getter
				if (prop != null && (Modifier.isStatic(m.getModifiers())==false) && (Modifier.isPublic(m.getModifiers())==true)
						&& checkSetAccessor(c, getMethodNameFromPropertyName("set", prop.getFirst()), prop.getSecond())
						&& (checkGetAccessors(c, getMethodNameFromPropertyName("get", prop.getFirst()), prop.getSecond())
								|| checkGetAccessors(c, getMethodNameFromPropertyName("is", prop.getFirst()), prop.getSecond())))
				{
					// If we're on a getX() and there's already been setX(), or viceversa, we reuse the same Mapping
					// and add to it whatever annotation we find
					
					Mapping candidate = null; // result.getPropertyMapping(prop.getFirst());
					for (Mapping map: resultMapping)
					{
						if (map.getName().equals(prop.getFirst()))
							candidate = map;
					}
					
					if (candidate == null)
					{
						candidate = new Mapping(c, prop.getFirst(), prop.getSecond(), mapper);						
					}

					setupMappingAnnotations(result, resultMapping, candidate, m.getAnnotations());
				}
			}
		}
				
		// Remove all the transients fields
		for (ListIterator<Mapping> i=resultMapping.listIterator(); i.hasNext(); )
		{
			if (i.next().isTransient())
				i.remove();
		}
		
		// Set up ids
		LinkedList<Mapping> ids = new LinkedList<Mapping>();
		boolean someKeyGenerated = false;
		boolean someKeyNotGenerated = false;
		for (Mapping m: resultMapping)
		{
			if (m.isKey())
			{
				ids.add(m);

				if (m.isKeyGenerated())
					someKeyGenerated = true;
				else
					someKeyNotGenerated = true;
			}
		}
		
		if (someKeyGenerated && someKeyNotGenerated)
		{
			throw new RuntimeException("Can't mix generated and not generated key in a multikey proxy for class " + result.getMappedClass() );
		}
		if (someKeyGenerated)
			result.setIdGenerated(true);
		else
			result.setIdGenerated(false);
		
		// merge the keyset
		
		Mapping[] idarray = new Mapping[ids.size()];
		for (int i=0; i<ids.size(); i++)
			idarray[i] = ids.get(i);
		
		result.setIds(merge(result.getIds(), idarray));
		
		for (Mapping m: resultMapping)
		{
			result.addMapping(m);
		}
		
		// Now it's clean up time: we remove all the transient mapping from the resulting proxy
//		List<Mapping> lst = result.getMappingListCopy(); 
//		for (Mapping m: lst)
//		{
//			if (m.isTransient())
//				result.removeMapping(m);
//		}
		
		return result;
	}

	static private Mapping[] merge(Mapping[] array1, Mapping[] array2)
	{
		if (array1 == null)
			return array2;
		if (array2 == null)
			return array1;
		
		Mapping[] result = new Mapping[array1.length + array2.length];
		for (int i=0; i<array1.length; i++)
			result[i] = array1[i];
		for (int i=0; i<array2.length; i++)
			result[array1.length+i] = array2[i];
		
		return result;
	}
	
	static private void setupMappingAnnotations(ClassProxy proxy, List<Mapping> mappingList, Mapping mapping, Annotation[] annotations)
	{
		boolean addMapping = true;

		boolean isId = false;
		boolean isGeneratedValue = false;

		for (Annotation a: annotations)
		{
			if (a instanceof Column)
			{
				mapping.setSqlName(((Column)a).value());
			}
			else if (a instanceof javax.persistence.Column)
			{
				String col = ((javax.persistence.Column)a).name();
				if (col != null && col.length()>0)
					mapping.setSqlName(col);
			}
			else if (a instanceof javax.persistence.Id)
			{
				isId = true;
			}
			else if (a instanceof javax.persistence.Transient)
			{
				mapping.setTransient(true);
			}
			else if (a instanceof javax.persistence.GeneratedValue)
			{
				isGeneratedValue = true;
			}
		}


		if (mapping.isValid())
		{
			if (isId)
				mapping.setKey(true);

			if (isId && isGeneratedValue)
				mapping.setKeyGenerated(true);
			
				//proxy.setIdGenerated(true);

			if (mappingList.contains(mapping) == false)
				mappingList.add(mapping);
//			proxy.addMapping(mapping);
		}
	}

	static private boolean isKnownAnnotation(Annotation[] anno)
	{
		for (Annotation a: anno)
		{
			if (a.annotationType().equals(Column.class) || a.annotationType().equals(javax.persistence.Column.class)
					|| a.annotationType().equals(javax.persistence.Id.class) || a.annotationType().equals(javax.persistence.Transient.class) )
				return true;
		}
		return false;
	}

	static public Pair<String,Class<?>> getPropertyFromMethodName(Method method)
	{
		String name = method.getName();

		String root = null;
		if (name.startsWith("get") && name.length()>3 && method.getParameterTypes().length==0 && !method.getReturnType().equals(Void.TYPE))
		{
			return new Pair<String,Class<?>>(name.substring(3, 4).toLowerCase()+name.substring(4), method.getReturnType());
		}

		if (name.startsWith("is") && name.length()>2 && method.getParameterTypes().length==0 && !method.getReturnType().equals(Void.TYPE))
		{
			return new Pair<String,Class<?>>(name.substring(2, 3).toLowerCase()+name.substring(3), method.getReturnType());
		}

		if (name.startsWith("set") && name.length()>3 && method.getParameterTypes().length==1 && method.getReturnType().equals(Void.TYPE))
		{
			return new Pair<String,Class<?>>(name.substring(3, 4).toLowerCase()+name.substring(4), method.getParameterTypes()[0]);
		}

		return null;
	}

	static public String getMethodNameFromPropertyName(String prefix, String name)
	{
		StringBuffer result = new StringBuffer();
		result.append(prefix);
		if (name != null && name.length()>0)
		{
			result.append(name.substring(0, 1).toUpperCase());
			result.append(name.substring(1));
		}
		return result.toString();
	}

	static public boolean checkGetAccessors(Class clazz, String methodName, Class type)
	{
		try {
			Method getter = clazz.getMethod(methodName);
			if (getter.getReturnType().equals(type) == false)
				return false;
			if ((Modifier.isStatic(getter.getModifiers())==true) || (Modifier.isPublic(getter.getModifiers())==false))
				return false;

			return true;
		} catch (Exception exc)
		{
			// ignore
		}
		return false;
	}

	static public boolean checkSetAccessor(Class clazz, String methodName, Class type)
	{
		try {
			Method setter = clazz.getMethod(methodName, type);
			Class<?> returnType = setter.getReturnType();
			if (!returnType.equals(Void.TYPE) && !returnType.equals(clazz))
				return false;

			if ((Modifier.isStatic(setter.getModifiers())==true) || (Modifier.isPublic(setter.getModifiers())==false))
				return false;
				
			return true;
			
		} catch (Exception exc)
		{
			// ignore
		}
		return false;
	}
}
